/*
*  Copyright (c) 2001 Sun Microsystems, Inc.  All rights
*  reserved.
*
*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions
*  are met:
*
*  1. Redistributions of source code must retain the above copyright
*  notice, this list of conditions and the following disclaimer.
*
*  2. Redistributions in binary form must reproduce the above copyright
*  notice, this list of conditions and the following discalimer in
*  the documentation and/or other materials provided with the
*  distribution.
*
*  3. The end-user documentation included with the redistribution,
*  if any, must include the following acknowledgment:
*  "This product includes software developed by the
*  Sun Microsystems, Inc. for Project JXTA."
*  Alternately, this acknowledgment may appear in the software itself,
*  if and wherever such third-party acknowledgments normally appear.
*
*  4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA"
*  must not be used to endorse or promote products derived from this
*  software without prior written permission. For written
*  permission, please contact Project JXTA at http://www.jxta.org.
*
*  5. Products derived from this software may not be called "JXTA",
*  nor may "JXTA" appear in their name, without prior written
*  permission of Sun.
*
*  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
*  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
*  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
*  DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
*  ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
*  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
*  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
*  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
*  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
*  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
*  SUCH DAMAGE.
*  ====================================================================
*
*  This software consists of voluntary contributions made by many
*  individuals on behalf of Project JXTA.  For more
*  information on Project JXTA, please see
*  <http://www.jxta.org/>.
*
*  This license is based on the BSD license adopted by the Apache Foundation.
*
*  $Id: EmoticonFilter.java,v 1.7 2007/05/28 22:00:52 nano Exp $
*/

package net.jxta.myjxta.dialog.filter;

import net.jxta.logging.Logging;
import net.jxta.myjxta.dialog.DialogMessage;
import net.jxta.myjxta.dialog.DialogMessageWrapper;
import net.jxta.myjxta.util.Constants;
import net.jxta.myjxta.util.Resources;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Substitues emoticon graphics for emoticon ascii sequences.
 * See net/jxta/myjxta/resources/themes/theme.txt for a list of emoticon sequences.
 * <pattern>
 * No substitution occurs if the emoticon sequence is preceeded by the escape character "/".
 *
 * @author james todd [gonzo at jxta dot org]
 * @author mike mcangus [mcangus at jxta dot org]
 * @version $Id: EmoticonFilter.java,v 1.7 2007/05/28 22:00:52 nano Exp $
 */

public final class EmoticonFilter extends AbstractDialogFilter {

    private static final Logger LOG = Logger.getLogger(EmoticonFilter.class.getName());
    private static final String defaultTheme =
            "/net/jxta/myjxta/resources/themes/crystal-gaim/theme.txt";
    private static final String DELIMITER = "/";
    private static final String EMOTICON_ESCAPE_CHAR = "/";
    private static final String ESCAPE_PATTERN_PATTERN = "\\";
    private static final String PREFIX = "<img src=\"";
    private static final String POSTFIX = "\">";

    // Small set of only the most often used HTML Character Entities
    private static final Set<String> CHAR_ENTITIES;

    static {
        CHAR_ENTITIES = new HashSet<String>(16);
        CHAR_ENTITIES.add("&amp");
        CHAR_ENTITIES.add("&copy");
        CHAR_ENTITIES.add("&gt");
        CHAR_ENTITIES.add("&lt");
        CHAR_ENTITIES.add("&nbsp");
        CHAR_ENTITIES.add("&quot");
        CHAR_ENTITIES.add("&reg");
    }

    private static final Map<String, List<Pattern>> patterns;

    // Loads the patterns map with emoticon graphic URL and a List of the emoticons for them.
    // Keeps the list in the same order as they appear in the theme.txt file.  This allows us to ensure that emoticons
    // that are extensions of other emoticons ( e.g., >:O extends :O ) are always displayed properly if they appear 
    // in the file before the emoticons that they extend.  (clear?)
    static {
        Constants c = Constants.getInstance();
        String theme = c.get(Constants.THEME_RESOURCES, defaultTheme);
        String base = theme.substring(0, theme.lastIndexOf(DELIMITER) + DELIMITER.length());
        InputStream is = Resources.class.getResourceAsStream(theme);
        patterns = new LinkedHashMap<String, List<Pattern>>();

        if (is != null) {
            StreamTokenizer st =
                    new StreamTokenizer(new BufferedReader(new InputStreamReader(is)));

            st.ordinaryChars('0', '9');
            st.wordChars('!', '~');
            st.commentChar('#');
            st.eolIsSignificant(true);

            try {
                String msgText = null;
                String t = null;
                String resource = null;
                boolean newLine = true;
                List<Pattern> emoticonList = null;
                Pattern pattern = null;

                while (st.nextToken() != StreamTokenizer.TT_EOF) {
                    if (st.ttype == StreamTokenizer.TT_WORD) {
                        msgText = st.sval;

                        if (newLine) {
                            newLine = false;
                            resource = base + msgText;
                            if (patterns.containsKey(resource)) {
                                // if the same resource was previously defined in the themes file
                                // we keep adding to its list of ascii emoticons.
                                emoticonList = patterns.get(resource);
                            } else {
                                emoticonList = new ArrayList<Pattern>();
                                patterns.put(resource, emoticonList);
                            }
                        } else {
                            t = escape(msgText);
                            try {
                                pattern = Pattern.compile(t);
                            } catch (PatternSyntaxException pse) {
                                LOG.log(Level.SEVERE,
                                        "Caught unexpected Exception", pse);
                                continue;
                            }
                            if (!exists(pattern)) {
                                emoticonList.add(pattern);
                            }
                        }
                    } else if (st.ttype == StreamTokenizer.TT_NUMBER) {
                        //ignore numbers}
                    } else if (st.ttype == StreamTokenizer.TT_EOL) {
                        newLine = true;
                    }
                }
            } catch (IOException ioe) {
                LOG.log(Level.SEVERE, "Caught unexpected Exception", ioe);
            }
        }
        if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
            LOG.fine("Map:patterns = [");
            for (Iterator<String> pit = patterns.keySet().iterator(); pit.hasNext();) {
                for (Iterator lit = patterns.get(pit.next()).iterator(); lit.hasNext();) {
                    LOG.fine("                " +
                            ((Pattern) lit.next()).pattern());
                }
            }
            LOG.fine("               ]");
        }
    }

    /**
     * Finds emoticon sequences in the message text, and substitutes the URL of the corresponding image.
     * Uses {@link java.util.regex.Pattern regex Pattern} and {@link java.util.regex.Matcher regex Matcher}
     * to find emoticon sequences.
     * <p/>
     * No substitution occurs if the emoticon sequence is preceeded by the escape character "/".
     * <p/>
     * Also, certain HTML Character Entities are check for to ensure that if they occur near a paren
     * that they don't cause extraneous emoticon images to be generated.
     *
     * @param msg the newly received DialogMessage object
     * @return the newly modified DialogMessage object.
     * @see net.jxta.myjxta.dialog.DialogFilter#filter(net.jxta.myjxta.dialog.DialogMessage)
     */
    public DialogMessage filter(DialogMessage msg) {
        if (!processMessage((DialogMessageWrapper) msg)) {
            return msg;
        }

        String msgText = msg.getHtmlMessage() != null ? msg.getHtmlMessage().trim() : "";
        LOG.fine("Begin filter(DialogMessage)");
        LOG.fine("msgText = " + msgText);

        if (msgText != null && msgText.length() > 0) {
            Pattern pattern = null;
            List patternList = null;
            Matcher matcher = null;
            String resource = null;
            URL emoticonUrl = null;
            StringBuffer workingBuff = new StringBuffer();

            for (Iterator<String> resources = patterns.keySet().iterator(); resources.hasNext();) {
                resource = resources.next();
                patternList = patterns.get(resource);
                for (Iterator patternIt = patternList.iterator(); patternIt.hasNext();) {
                    pattern = (Pattern) patternIt.next();
                    matcher = pattern.matcher(msgText);
                    if (matcher.find()) {
                        String match = matcher.group();
                        String[] nonMatchingText = pattern.split(msgText);

                        if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
                            LOG.fine("Found matching text \"" + match +
                                    "\" for pattern \"" + pattern.pattern() +
                                    "\"");
                            StringBuffer nmtString = new StringBuffer("[").append(Constants.CRLF).append("\"").append(nonMatchingText[0]).append("\"");

                            for (int i = 1; i < nonMatchingText.length; i++) {
                                nmtString.append(",").append(Constants.CRLF).append("\"").append(nonMatchingText[i]).append("\"");
                            }
                            nmtString.append(Constants.CRLF).append("]");
                            LOG.fine("nonMatchingText = " + nmtString.toString());
                            LOG.fine("nonMatchingText.length = " +
                                    nonMatchingText.length);
                        }
                        emoticonUrl = Resources.class.getResource(resource);
                        String imageTag = PREFIX + emoticonUrl + POSTFIX;
                        int textIndex = 0;

                        while (textIndex < nonMatchingText.length - 1) {
                            if (nonMatchingText[textIndex].endsWith(EMOTICON_ESCAPE_CHAR)) {
                                LOG.fine("Escaping emoticon");
                                workingBuff.append(nonMatchingText[textIndex].substring(0,
                                        nonMatchingText[textIndex].length() -
                                                1));
                                workingBuff.append(match);
                            } else {
                                int ampX = nonMatchingText[textIndex].lastIndexOf('&');

                                if (match.startsWith(";") && ampX >= 0) {
                                    String testEntityChar = nonMatchingText[textIndex].substring(ampX);

                                    if (CHAR_ENTITIES.contains(testEntityChar)) {
                                        LOG.fine("Character Entity, not real emoticon");
                                        workingBuff.append(nonMatchingText[textIndex]);
                                        workingBuff.append(match);
                                    } else {
                                        LOG.fine("Substituting emoticon");
                                        workingBuff.append(nonMatchingText[textIndex]).append(imageTag).append("&nbsp;");
                                    }
                                } else {
                                    LOG.fine("Substituting emoticon");
                                    workingBuff.append(nonMatchingText[textIndex]).append(imageTag).append("&nbsp;");
                                }
                            }
                            textIndex++;
                        }
                        workingBuff.append(nonMatchingText[textIndex]);
                        // update the msgText and reset the working text for the next emoticon
                        msgText = workingBuff.toString();
                        workingBuff.setLength(0);
                    }
                }
            }

            msg.setHtmlMessage(msgText);
        }

        if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
            LOG.fine("returning: " + msgText);
            LOG.fine("End   filter(DialogMessage)");
        }
        return msg;
    }

    private static boolean exists(Pattern msgPat) {
        for (String key : patterns.keySet()) {
            Set<Pattern> patternSet = new HashSet<Pattern>(patterns.get(key));
            if (patternSet.contains(msgPat)) {
                return true;
            }
        }

        return false;
    }

    private static String escape(String s) {
        if (s == null) {
            return null;
        }

        char[] c = s.toCharArray();
        StringBuffer sb = new StringBuffer();

        for (int i = 0; i < c.length; i++) {
            if (!Character.isLetterOrDigit(c[i])) {
                sb.append(ESCAPE_PATTERN_PATTERN);
            }

            sb.append(c[i]);
        }

//        return c != null ? (ESCAPE_EMOTICON_PATTERN + "(" + sb.toString() + ")") : s;
//        return c != null ? (ESCAPE_EMOTICON_PATTERN + "(" + CHAR_ENTITIES_PATTERN + sb.toString() + ")") : s;
        return sb.toString();
    }
}
